import { dom, EventHandler, Nodes } from '@knno/dom';
import { isDescendant } from '../../tools/utils';
import { Container } from '../interface';
import { popupStyle } from './popup.css';
import { DATA } from '../symbol';

type BesideH = 'left' | 'right';
type BesideV = 'top' | 'bottom';
type Beside = BesideH | BesideV;
type AlignH = 'left' | 'right' | 'center';
type AlingV = 'top' | 'bottom' | 'middle';
type Align = AlignH | AlingV;
export type PopupPosition = `${BesideH}-${AlingV}` | `${BesideV}-${AlignH}`;

type Rect = {
	x: number;
	y: number;
	width: number;
	height: number;
};

type CalcBeside = {
	side: Beside;
	value: number;
};

type CalcAlign = {
	align: Align;
	value: number;
};

function besideLeft(ref: Rect, rect: Rect): CalcBeside {
	let side: Beside = 'left';
	let x = ref.x - rect.width;
	if (x < 0 && ref.x + ref.width + rect.width < document.documentElement.offsetWidth) {
		side = 'right';
		x = ref.x + ref.width;
	}
	return {
		side,
		value: x,
	};
}

function besideRight(ref: Rect, rect: Rect): CalcBeside {
	let side: Beside = 'right';
	let x = ref.x + ref.width;
	if (x + rect.width > document.documentElement.offsetWidth && ref.x - rect.width > 0) {
		side = 'left';
		x = ref.x - rect.width;
	}
	return {
		side,
		value: x,
	};
}

function besideTop(ref: Rect, rect: Rect): CalcBeside {
	let side: Beside = 'top';
	let y = ref.y - rect.height;
	if (y < 0 && ref.y + ref.height + rect.height < window.innerHeight) {
		side = 'bottom';
		y = ref.y + ref.height;
	}
	return {
		side,
		value: y,
	};
}

function besideBottom(ref: Rect, rect: Rect): CalcBeside {
	let side: Beside = 'bottom';
	let y = ref.y + ref.height;
	if (y + rect.height > window.innerHeight && ref.y - rect.height > 0) {
		side = 'top';
		y = ref.y - rect.height;
	}
	return {
		side,
		value: y,
	};
}

function alignLeft(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'left';
	let x = ref.x;
	if (x + rect.width > document.documentElement.offsetWidth && ref.x + ref.width - rect.width > 0) {
		align = 'right';
		x = ref.x + ref.width - rect.width;
	}
	return {
		align,
		value: x,
	};
}

function alignRight(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'right';
	let x = ref.x + ref.width - rect.width;
	if (x < 0 && ref.x + ref.width < document.documentElement.offsetWidth) {
		align = 'left';
		x = ref.x;
	}
	return {
		align,
		value: x,
	};
}

function alignCenter(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'center';
	let x = ref.x + (ref.width - rect.width) / 2;
	if (x < 0 && ref.x + ref.width < document.documentElement.offsetWidth) {
		align = 'left';
		x = ref.x;
	} else if (x + rect.width > document.documentElement.offsetWidth && ref.x + ref.width - rect.width > 0) {
		align = 'right';
		x = ref.x + ref.width - rect.width;
	}
	return {
		align,
		value: x,
	};
}

function alignTop(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'top';
	let y = ref.y;
	if (y + rect.height > window.innerHeight && ref.y + ref.height - rect.height > 0) {
		align = 'bottom';
		y = ref.y + ref.height - rect.height;
	}
	return {
		align,
		value: y,
	};
}

function alignBottom(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'bottom';
	let y = ref.y + ref.height - rect.height;
	if (y < 0 && ref.y + ref.height < window.innerHeight) {
		align = 'top';
		y = ref.y;
	}
	return {
		align,
		value: y,
	};
}

function alignMiddle(ref: Rect, rect: Rect): CalcAlign {
	let align: Align = 'middle';
	let y = ref.y + (ref.height - rect.height) / 2;
	if (y < 0 && ref.y + ref.height < window.innerHeight) {
		align = 'top';
		y = ref.y;
	} else if (y + rect.height > window.innerHeight && ref.y + ref.height - rect.height > 0) {
		align = 'bottom';
		y = ref.y + ref.height - rect.height;
	}
	return {
		align,
		value: y,
	};
}

export interface PopupEventMap {
	open: Event;
	close: Event;
}
export type PopupEventKey = keyof PopupEventMap;
export type PopupEventHandler<T extends PopupEventKey, O extends Nodes> = (this: O, event: PopupEventMap[T]) => void;
interface PopupBox {
	on<K extends keyof PopupEventMap>(
		event: K,
		listener: PopupEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	on<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof PopupEventMap>(
		event: K,
		listener: PopupEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
}

export type Pos = { x: number; y: number };

export type ShowOptions = {
	refer: HTMLElement | Nodes;
	position?: PopupPosition | Pos;
	focus?: boolean;
	/**
	 * Whether use bubble style.
	 */
	bubbleStyle?: boolean;
};

class PopupBox extends Container {
	private [DATA]: {
		visible: boolean;
		binding?: HTMLElement;
		beside?: Beside;
		align?: Align;
		calcSide?: Beside;
		calcAlign?: Align;
		pos?: Pos;
		bubbleStyle?: boolean;
	};

	protected get box(): Nodes {
		return this.query(':scope>div.panel');
	}

	private onFocusChange: () => void;
	constructor() {
		const box = dom.div(popupStyle).append(dom.div('panel')).attr('tabindex', '0');
		super(box);
		this[DATA] = { visible: false };
		this.onFocusChange = () => {
			setTimeout(() => {
				if (!document.activeElement || !isDescendant(this.elems(), document.activeElement)) {
					this.close();
				}
			});
		};
		this.on('keydown', (evt) => {
			if (evt.key === 'Escape') {
				evt.stopPropagation();
				this.close();
				this[DATA].binding?.focus();
			}
		});
		this.on('contextmenu', (evt) => {
			evt.preventDefault();
		});
	}

	showUp(focus: boolean) {
		if (!this[DATA].visible) {
			this[DATA].visible = true;
			let parent = document.body;
			let elem: HTMLElement | undefined | null = this[DATA].binding;
			while (elem) {
				if (dom.wrap(elem).class().includes(popupStyle)) {
					parent = elem;
					break;
				} else {
					elem = elem.parentElement;
				}
			}
			this.appendTo(parent);
			focus && this.focus();
			document.documentElement.addEventListener('focusin', this.onFocusChange);
			document.documentElement.addEventListener('mousedown', this.onFocusChange);
			setTimeout(() => {
				this.addClass('active');
				this.emit(new Event('open'));
			});
			this.position();
		}
	}

	noIndicator(value: boolean) {
		value && this.addClass('no-indicator');
		return this;
	}

	show(refer: HTMLElement | Nodes, position: PopupPosition | Pos = 'bottom-center', focus = true) {
		this[DATA].binding = refer instanceof Nodes ? (refer[0] as HTMLElement) : refer;
		if (typeof position === 'string') {
			this[DATA].beside = position.split('-')[0] as Beside;
			this[DATA].align = position.split('-')[1] as Align;
			this[DATA].pos = undefined;
		} else {
			this[DATA].pos = position;
			this[DATA].beside = undefined;
			this[DATA].align = undefined;
		}
		this.showUp(focus);
		return this;
	}

	close() {
		if (this[DATA].visible) {
			this[DATA].visible = false;
			this.removeClass('active');
			document.documentElement.removeEventListener('focusin', this.onFocusChange);
			document.documentElement.removeEventListener('mousedown', this.onFocusChange);
			setTimeout(() => {
				this.detach();
				this.emit(new Event('close'));
			}, 200);
		}
		return this;
	}

	private calcAlignV(ref: Rect, rect: Rect): CalcAlign {
		if (this[DATA].align === 'top') {
			return alignTop(ref, rect);
		} else if (this[DATA].align === 'bottom') {
			return alignBottom(ref, rect);
		} else {
			return alignMiddle(ref, rect);
		}
	}

	private calcAlignH(ref: Rect, rect: Rect): CalcAlign {
		if (this[DATA].align === 'left') {
			return alignLeft(ref, rect);
		} else if (this[DATA].align === 'right') {
			return alignRight(ref, rect);
		} else {
			return alignCenter(ref, rect);
		}
	}

	private position() {
		if (this[DATA].visible) {
			this.css({
				left: '0px',
				top: '0px',
			});
			const rect: Rect = this.elems()[0].getBoundingClientRect();
			if (this[DATA].pos) {
				let { x, y } = this[DATA].pos;
				if (x + rect.width > document.documentElement.offsetWidth) {
					if (x - rect.width >= 0) {
						x -= rect.width;
					} else {
						x = document.documentElement.offsetWidth - rect.width;
					}
				}
				if (y + rect.height > document.documentElement.offsetHeight) {
					if (x - rect.height >= 0) {
						y -= rect.height;
					} else {
						x = document.documentElement.offsetHeight - rect.height;
					}
				}
				this.css({
					left: x + 'px',
					top: y + 'px',
				});
				this.attr({
					side: null,
					align: null,
				});
			} else {
				const ref: Rect = this[DATA].binding
					? this[DATA].binding.getBoundingClientRect()
					: { x: 0, y: 0, width: 0, height: 0 };
				let side: CalcBeside;
				let align: CalcAlign;
				if (this[DATA].beside === 'left') {
					side = besideLeft(ref, rect);
					align = this.calcAlignV(ref, rect);
					this.css({
						left: side.value + 'px',
						top: align.value + 'px',
					});
				} else if (this[DATA].beside === 'right') {
					side = besideRight(ref, rect);
					align = this.calcAlignV(ref, rect);
					this.css({
						left: side.value + 'px',
						top: align.value + 'px',
					});
				} else if (this[DATA].beside === 'top') {
					side = besideTop(ref, rect);
					align = this.calcAlignH(ref, rect);
					this.css({
						left: align.value + 'px',
						top: side.value + 'px',
					});
				} else {
					side = besideBottom(ref, rect);
					align = this.calcAlignH(ref, rect);
					this.css({
						left: align.value + 'px',
						top: side.value + 'px',
					});
				}
				if (this[DATA].calcSide !== side.side || this[DATA].calcAlign !== align.align) {
					this[DATA].calcSide = side.side;
					this[DATA].calcAlign = align.align;
					this.attr({
						side: side.side,
						align: align.align,
					});
				}
			}
			requestAnimationFrame(this.position.bind(this));
		}
	}
}

export const Popup = {
	create(...elem: (Text | Element | (Element | Text)[])[]): PopupBox {
		const pop = new PopupBox();
		pop.append(...elem);
		return pop;
	},
};
